function [runnable, list] = configurePlexIntTask
% Set up to run the Snow Dots Plexon Integration demo task.
%
%   [runnable, list] = configurePlexIntTask
%
%   The Snow Dots Plexon Integration demo task demonstrates communication
%   between Snow Dots and Plexon, to do online data access and analysis
%   during a task.  It uses dotsTheComputablesManager and
%   dotsComputableFunction objects to make a bridge between the Snow Dots
%   client, which is controlling the task, and a Snow Dots server, which
%   has access to Plexon.
%
%   The client and server counterparts of dotsTheComputablesManager may run
%   on the same machine or different machines, as required, as long as the
%   client and server modes, IP addresses, and ports, are correctly
%   configured with dotsTheMachineConfiguration.
%
%   Note that the Plexon Integration task could run on one, two, or three
%   distinct computers: there is always one Snow Dots client, and either of
%   the server counterparts for dotsTheComputablesManager or
%   dotsTheDrawablesManager may run on the same or a distinct comuter.
%
%   The returned runnable object is a topsRunnable which organizes flow
%   through tasks and trials. runnable.run() will start the task.
%   runnable.gui() will launch a graphical interface for viewing the
%   organization of the task.
%
%   The returned list object is a topsGroupedList which holds all the
%   objects and data needed to run the task.  list.gui() will launch a
%   graphical interface for viewing the objects and data.
%
%   The task uses graphical stimuli including targets, a fixation point,
%   and a random dot kinetogram.  The subject is expected to decide towards
%   which target the dots in the kinetogram appeared to be moving.
%
%   The task uses a HID keyboard to take input.  The subject may press the
%   left or right arrow at any time.  Key-presses that occur while targets
%   are on the screen will be counted as subject responses.
%
%   The subjerct will see "correct", "incorrect", or "no respone" feedback
%   at the end of each trial.  Otherwise, responses will not affect the
%   flow and timing of the task.  Thus, the task should be suitable as an
%   unattended integreation test of Snow Dots-Plexon integration.
%
%   The task contains multiple trials, each containing one stimulus
%   presentation and one decision.  Each trial follows the same sequence:
%       - the screen starts out blank
%       - the fixation point appears on the screen during a "preparation"
%       time interval
%       - the dots kinetogram also appears on the screen and presents a
%       motion signal during a "stimulus" time interval
%       - the dots kinetogram is replaced on the screen by two blue targets
%       during is a "choice" time interval, from which subject responses
%       are considered
%       - the screern goes blank for a short interval
%       - text indicating the trial outcome apppears on the screen during a
%       "feedback" interval
%       - the screen goes blank during an "intertrial" interval
%       .
%
%   The dot motion direction and subject responses are logged with
%   topsDataLog.  Data about the flow of events during the task are also
%   logged.
%
%   During each trial, two kinds of data are sent to Plexon:
%       -# at the start of each trial, a strobed digital word is sent from
%       a dotsDOut* object to the Plexon digital inputs.  Each word
%       represents the current trial number.  A timestamp for each strobe
%       event is logged in topsDataLog.
%       -# near the start of each trial, an arbitrary TTL signal is sent
%       using the same dotsDOut* object.  The output should be connected to
%       a Plexon analog input, just for demonstration purposes.  A
%       timestamp for the start of each TTL signal is logged in
%       topsDataLog.
%       .
%
%   Near the end of each trial, after the "choice" interval and before
%   feedback appears, the client and server counterparts of
%   dotsTheComputablesManager exchange two kinds of data:
%       -# recent data from topsDataLog are sent from the client side to
%       the server side, and added to the server-side instance of
%       topsDataLog
%       -# the outcome of the trial is requested from the client side,
%       computed on the server side, and returned to the client side.
%       .
%
%   Task parameters, such as the lengths of eacvh time interval and the
%   total number of trials, may be edited near the top of this file,
%   configurePlexIntTask.m.
%

% Global Matlab state
topsDataLog.flushAllData;
log = topsDataLog.theDataLog;
dotsTheSwitchboard.sharePropertyOfObject('clockFunction', log);

dotsTheComputablesManager.reset;
dotsTheDrawablesManager.reset;
dotsTheQueryablesManager.reset;

%% Constants:
taskName = 'plexInt';
list = topsGroupedList;

% add some reusable "ideas"
output = list.addIdea('output');
preparation = list.addIdea('preparation');
stimulus = list.addIdea('stimulus');
choice = list.addIdea('choice');
compute = list.addIdea('compute');
feedback = list.addIdea('feedback');
intertrial = list.addIdea('intertrial');

quit = list.addIdea('quit');

timing = list.addIdea('timing');
list{timing}{preparation} = .25;
list{timing}{stimulus} = .25;
list{timing}{choice} = .25;
list{timing}{feedback} = .25;
list{timing}{intertrial} = .25;

list{'control'}{'trials per condition'} = 2;

%% Graphics:
dm = dotsTheDrawablesManager.theObject;
dm.name = 'draw graphics';
fp = dm.newObject('dotsDrawableTargets');
list{'graphics'}{'fixation point'} = fp;

stim = dm.newObject('dotsDrawableDotKinetogram');
list{'graphics'}{'stimulus'} = stim;

targs = dm.newObject('dotsDrawableTargets');
list{'graphics'}{'targets'} = targs;

fb = dm.newObject('dotsDrawableText');
list{'graphics'}{'feedback'} = fb;

dm.addObjectToGroup(fp, taskName);
dm.addObjectToGroup(stim, taskName);
dm.addObjectToGroup(targs, taskName);
dm.addObjectToGroup(fb, taskName);

%% Computations:
cm = dotsTheComputablesManager.theObject;
cm.name = 'do computations';
plotter = cm.newObject('dotsComputableFunction');
plotter.prepareFunction = @()plexIntShowAnalog('init');
plotter.computeFunction = @plexIntShowAnalog;
plotter.recomputeInterval = 1;
list{'computations'}{'plotter'} = plotter;

results = cm.newObject('dotsComputableFunction');
results.computeFunction = @plexIntTrialResult;
list{'computations'}{'results'} = results;

%% Input:
qm = dotsTheQueryablesManager.theObject;
kb = qm.newObject('dotsQueryableHIDKeyboard');
list{'input'}{'keyboard'} = kb;
leftArrow = kb.phenomenons{'pressed'}{'pressed_KeyboardLeftArrow'};
rightArrow = kb.phenomenons{'pressed'}{'pressed_KeyboardRightArrow'};
escape = kb.phenomenons{'pressed'}{'pressed_KeyboardEscape'};

% classify phenomenon -> idea
kb.addClassificationInGroupWithRank(leftArrow, 180, taskName, 2);
kb.addClassificationInGroupWithRank(rightArrow, 0, taskName, 3);
kb.addClassificationInGroupWithRank(escape, 'quit', taskName, 4);
kb.activeClassificationGroup = taskName;

%% Output:
dOutClassname = dotsTheSwitchboard.getSharedPropertyValue('dOutClassname');
dOut = feval(dOutClassname);
list{'output'}{'digital output'} = dOut;
list{'output'}{'TTL channel'} = 0;
list{'output'}{'TTL signal'} = [false, true(1,30), true(1,0*32), false];
list{'output'}{'TTL frequency'} = 1000;

%% Control
conditions = topsConditions;
conditions.name = 'pick left or right';
conditions.addParameter('direction', {0, 180});
conditions.addAssignment('direction', stim, '.', 'direction');
conditions.setPickingMethod('shuffled', list{'control'}{'trials per condition'});
list{'control'}{'conditions'} = conditions;

calls = topsCallList;
calls.name = 'call some functions';
calls.alwaysRunning = true;
calls.addCall({@readData, kb});
list{'control'}{'calls'} = calls;

states = topsStateMachine;
states.name = 'traverse trial states';
stateInfo = { ...
    'name',         'next',     'entry',        'exit',         'timeout'; ...
    output,         preparation,{@doOutput, list},{},           0
    preparation,	stimulus,	{@show, fp},	{},             list{timing}{preparation}; ...
    stimulus,   	choice,     {@show, stim},	{@hide, stim},	list{timing}{stimulus}; ...
    choice,     	compute,    {@show, targs},	{@hide, targs},	list{timing}{choice}; ...
    compute,        feedback,   {@hide, fp},    {@getResult, list},0; ...
    feedback,   	intertrial,	{@show, fb},	{@hide, fb},	list{timing}{feedback}; ...
    intertrial,     '',         {},             {},             0; ...
    };
states.addMultipleStates(stateInfo);
states.startFevalable = {@mayDrawNextFrame, dm, true};
states.finishFevalable = {@mayDrawNextFrame, dm, false};

list{'control'}{'states'} = states;

together = topsConcurrentComposite;
together.name = ('run() together');
together.addChild(calls);
together.addChild(states);
together.addChild(dm);
together.addChild(cm);
list{'control'}{'together'} = together;

runnable = topsTreeNode;
runnable.name = taskName;
runnable.startFevalable = {@startTrials, list};
runnable.finishFevalable = {@finishTrials, list};
runnable.iterations = inf;
runnable.addChild(conditions);
runnable.addChild(together);
list{'control'}{'runnable'} = runnable;

%% Glue functions
function myShow(drawable)
drawable.isVisible = true;

function myHide(drawable)
drawable.isVisible = false;

function startTrials(list)
topsDataLog.flushAllData;
dotsTheComputablesManager.sendDataLogUpdate;

cm = dotsTheComputablesManager.theObject;
cm.activateGroup(cm.defaultGroup);

dm = dotsTheDrawablesManager.theObject;
dm.activateGroup(dm.defaultGroup);
dm.openScreenWindow;

function finishTrials(list)
cm = dotsTheComputablesManager.theObject;
cm.activateGroup('none');

dotsTheComputablesManager.sendDataLogUpdate;
plotter = list{'computations'}{'plotter'};
cm.mayComputeComputable(plotter);
plotter.computeInputs = {'close'};
cm.mayComputeComputable(plotter);

dm = dotsTheDrawablesManager.theObject;
dm.activateGroup('none');
dm.closeScreenWindow;

function doOutput(list)
% send a strobed word with the trial condition number
dOut = list{'output'}{'digital output'};

% send an arbitrary TTL signal
signal = list{'output'}{'TTL signal'};
timestamp = dOut.sendTTLSignal( ...
    list{'output'}{'TTL channel'}, ...
    list{'output'}{'TTL signal'}, ...
    list{'output'}{'TTL frequency'});
topsDataLog.logDataInGroup(signal, 'dOut TTL signal', timestamp);

conditions = list{'control'}{'conditions'};
word = conditions.currentCondition;
timestamp = dOut.sendStrobedWord(word);
topsDataLog.logDataInGroup(word, 'dOut strobed word', timestamp);


function getResult(list)
% query through tracked keyboard data
%   log the data and the query resyults
kb = list{'input'}{'keyboard'};
output = kb.queryNextTracked;
while ~isequal(output, kb.upToDateOutput)
    output = kb.queryNextTracked;
    if ischar(output) && strcmp(output, list{'ideas'}{'quit'})
        quitTask;
    end
end
kb.logAllData;
kb.logQueryOutputs;
kb.flushData;

% send a log update to the server
dotsTheComputablesManager.sendDataLogUpdate;

% determine the time range for considering subject responses:
%   I know that topsStateMachine makes topsDataLog entries for state entry
%   and exit functions, but I don't know the exact group name of the log
%   entries.  So I search for them with regular expressions.  I have to
%   know that "enter" or "exit" comes first, then the name of the state.  I
%   separate the two with the "." wildcard.
log = topsDataLog.theDataLog;
startChoiceGroup = log.getGroupNamesMatchingExpression('enter.choice');
finishChoiceGroup = log.getGroupNamesMatchingExpression('exit.choice');
if isempty(startChoiceGroup) || isempty(finishChoiceGroup)
    timeRange = [-inf inf];
else
    startChoiceData = log.getAllItemsFromGroupAsStruct(startChoiceGroup{1});
    finishChoiceData = log.getAllItemsFromGroupAsStruct(finishChoiceGroup{1});
    lastStartChoice = max([startChoiceData.mnemonic]);
    lastFinishChoice = max([finishChoiceData.mnemonic]);
    timeRange = [lastStartChoice, lastFinishChoice];
end

% get the trial result from the dotsTheComputablesManager server
%   log it.
cm = dotsTheComputablesManager.theObject;
results = list{'computations'}{'results'};
results.computeInputs = {timeRange};
trialResult = cm.mayComputeComputable(results);
topsDataLog.logDataInGroup(trialResult, 'trialResult');

% copy the result to the "fb" text object
fb = list{'graphics'}{'feedback'};
fb.string = trialResult;


function quitTask()
dm = dotsTheDrawablesManager.theObject;
dm.mayDrawNextFrame(false);
error('User quit the task')